#include "engine_v8.h"
#ifdef WIN32
#pragma warning (disable : 4005)
#pragma warning (disable : 4996)
#include<winsock2.h>
#include <windows.h>
#pragma comment(lib, "Ws2_32.lib")
#else
#include <unistd.h>
#endif
#include <uv.h>

namespace ccengin {

v8::Persistent<v8::Function> EngineWrapper::constructor;

EngineWrapper::EngineWrapper() {}
EngineWrapper::~EngineWrapper() {}

auto STR_ENGINE_LOCKED = "An finding process are executing, engine are locked!";

// ----------------------------- METHODS --------------------------------------

void EngineWrapper::startup(const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();
  bool value = args[0]->IsUndefined() ? 0 : args[0]->BooleanValue();
  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  if (obj->finding) {
    isolate->ThrowException(v8::Exception::Error(
        v8::String::NewFromUtf8(isolate, STR_ENGINE_LOCKED)));
    return;
  }
  obj->engine.startup(value ? 1 : 0);
}

void EngineWrapper::move(const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();

  byte params[4];
  if (args.Length() == 1 && args[0]->IsUint8Array()) {
    v8::Local<v8::Uint8Array> arr = args[0].As<v8::Uint8Array>();
    if (arr->Length() != 4) {
      isolate->ThrowException(v8::Exception::TypeError(v8::String::NewFromUtf8(
          isolate, "Invalid call of 'move': should provide "
                   "UInt8Array(length:4) as arguments.")));
      return;
    }
    arr->CopyContents(params, 4);
  } else if (args.Length() == 4) {
    char buffer[256];
    for (int i = 0; i < 4; i++) {
      if (!args[i]->IsInt32()) {
        sprintf(buffer,
                "Invalid call of 'move': invalid arguments: the argument %d "
                "should be integer.",
                i);
        isolate->ThrowException(
            v8::Exception::TypeError(v8::String::NewFromUtf8(isolate, buffer)));
        return;
      }
      int32_t v = args[i]->Int32Value();
      if ((i % 2 == 0) && (v < 0 || v > 8)) {
        sprintf(buffer,
                "Invalid call of 'move': invalid arguments: the argument %d "
                "value should be 0 to 8.",
                i);
        isolate->ThrowException(
            v8::Exception::TypeError(v8::String::NewFromUtf8(isolate, buffer)));
        return;
      } else if ((i % 2 == 1) && (v < 0 || v > 9)) {
        sprintf(buffer,
                "Invalid call of 'move': invalid arguments: the argument %d "
                "value should be 0 to 9.",
                i);
        isolate->ThrowException(
            v8::Exception::TypeError(v8::String::NewFromUtf8(isolate, buffer)));
        return;
      }
      params[i] = (byte)v;
    }
  } else {
    isolate->ThrowException(v8::Exception::TypeError(v8::String::NewFromUtf8(
        isolate, "Invalid call of 'move': should provide 4 numbers or an "
                 "UInt8Array as arguments.")));
    return;
  }

  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  if (obj->finding) {
    isolate->ThrowException(v8::Exception::Error(
        v8::String::NewFromUtf8(isolate, STR_ENGINE_LOCKED)));
    return;
  }
  bool ret = obj->engine.move(params[0], params[1], params[2], params[3]);
  args.GetReturnValue().Set(v8::Boolean::New(isolate, ret));
}

void EngineWrapper::undo(const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();
  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  if (obj->finding) {
    isolate->ThrowException(v8::Exception::Error(
        v8::String::NewFromUtf8(isolate, STR_ENGINE_LOCKED)));
    return;
  }
  obj->engine.undo();
}

void EngineWrapper::getMoveCount(
    const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();
  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  if (obj->finding) {
    isolate->ThrowException(v8::Exception::Error(
        v8::String::NewFromUtf8(isolate, STR_ENGINE_LOCKED)));
    return;
  }
  int moveCount = obj->engine.getMoveCount();
  args.GetReturnValue().Set(v8::Int32::New(isolate, moveCount));
}

void EngineWrapper::getPlayer(const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();
  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  if (obj->finding) {
    isolate->ThrowException(v8::Exception::Error(
        v8::String::NewFromUtf8(isolate, STR_ENGINE_LOCKED)));
    return;
  }
  int player = obj->engine.getPlayer();
  args.GetReturnValue().Set(v8::Int32::New(isolate, player));
}

void EngineWrapper::getState(const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();
  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  if (obj->finding) {
    isolate->ThrowException(v8::Exception::Error(
        v8::String::NewFromUtf8(isolate, STR_ENGINE_LOCKED)));
    return;
  }

  v8::Local<v8::ArrayBuffer> board = v8::ArrayBuffer::New(isolate, 90);
  int state = obj->engine.getState((chess::byte *)board->GetContents().Data());
  int realState = (state & 0x0F);
  bool over = (realState == 3 || realState == 4 || realState == 5);
  v8::Local<v8::Object> retObj = v8::Object::New(isolate);
  retObj->Set(v8::String::NewFromUtf8(isolate, "over"),
              v8::Boolean::New(isolate, over));
  if (realState == 1) {
    retObj->Set(v8::String::NewFromUtf8(isolate, "captured"),
                v8::Boolean::New(isolate, true));
  } else if (realState == 2) {
    retObj->Set(v8::String::NewFromUtf8(isolate, "checked"),
                v8::Boolean::New(isolate, true));
  } else if (realState == 3) {
    retObj->Set(v8::String::NewFromUtf8(isolate, "winner"),
                v8::String::NewFromUtf8(isolate, "red"));
  } else if (realState == 4) {
    retObj->Set(v8::String::NewFromUtf8(isolate, "winner"),
                v8::String::NewFromUtf8(isolate, "black"));
  }

  if (state & chess::Engine::REPEATED) {
    retObj->Set(v8::String::NewFromUtf8(isolate, "repeated"),
                v8::Boolean::New(isolate, true));
  } else if (state & chess::Engine::EXCEEDED_100) {
    retObj->Set(v8::String::NewFromUtf8(isolate, "exceeded"),
                v8::Boolean::New(isolate, true));
  }
  v8::Local<v8::Uint8Array> boardArray = v8::Uint8Array::New(board, 0, 90);
  retObj->Set(v8::String::NewFromUtf8(isolate, "board"), boardArray);

  args.GetReturnValue().Set(retObj);
}

void EngineWrapper::findSolutionSync(
    const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();
  float searchSeconds = 3;
  if (args.Length() >= 1) {
    if (args[0]->IsNumber()) {
      double v = args[0]->NumberValue();
      if (v > 0 && v <= 60) {
        searchSeconds = (float)v;
      }
    }
  }

  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  if (obj->finding) {
    isolate->ThrowException(v8::Exception::Error(
        v8::String::NewFromUtf8(isolate, STR_ENGINE_LOCKED)));
    return;
  }
  obj->finding = true;
  int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
  bool result = obj->engine.findSolution(searchSeconds, x1, y1, x2, y2);
  if (result) {
    v8::Local<v8::ArrayBuffer> movement = v8::ArrayBuffer::New(isolate, 4);
    unsigned char *data = (unsigned char *)movement->GetContents().Data();
    data[0] = x1;
    data[1] = y1;
    data[2] = x2;
    data[3] = y2;
    args.GetReturnValue().Set(v8::Uint8Array::New(movement, 0, 4));
  } else {
    args.GetReturnValue().SetNull();
  }
  obj->finding = false;
}

typedef struct Work {
  // required
  uv_work_t request;                // libuv
  v8::Persistent<v8::Function> callback; // javascript callback

	EngineWrapper *obj;
  float seconds;
	bool result;
	int x1;
	int y1;
	int x2;
	int y2;
} Work;

static void findProc(uv_work_t *req) {
  Work *work = static_cast<Work *>(req->data);
  work->result = work->obj->findSolutionDirect(work->seconds, work->x1, work->y1, work->x2, work->y2);
}

static void findOverProc(uv_work_t *req, int status) {
  Work *work = static_cast<Work *>(req->data);
	work->obj->findOver();
	v8::Isolate* isolate = v8::Isolate::GetCurrent();
	if (work->result) {
    v8::Local<v8::ArrayBuffer> movement = v8::ArrayBuffer::New(isolate, 4);
    unsigned char *data = (unsigned char *)movement->GetContents().Data();
    data[0] = work->x1;
    data[1] = work->y1;
    data[2] = work->x2;
    data[3] = work->y2;
		v8::Local<v8::Value> argv[1] = {v8::Uint8Array::New(movement, 0, 4)};
		v8::Local<v8::Function>::New(isolate, work->callback)->Call(isolate->GetCurrentContext()->Global(), 1, argv);
  } else {
		//v8::Local<v8::Value> argv[0] = {};
    v8::Local<v8::Function>::New(isolate, work->callback)->Call(isolate->GetCurrentContext()->Global(), 0, nullptr);
  }
	work->callback.Reset();
	delete work;
}

void EngineWrapper::findSolution(
    const v8::FunctionCallbackInfo<v8::Value> &args) {

  v8::Isolate *isolate = args.GetIsolate();
  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  if (obj->finding) {
    isolate->ThrowException(v8::Exception::Error(
        v8::String::NewFromUtf8(isolate, STR_ENGINE_LOCKED)));
    return;
  }
  obj->finding = true;

  Work *work = new Work;
	work->obj = obj;
  work->request.data = work;
  if (args.Length() >= 1 && args[0]->IsNumber()) {
    work->seconds = (float)args[0]->NumberValue();
  } else {
    work->seconds = 1;
  }
  if (args.Length() == 1 && args[0]->IsFunction()) {
		work->callback.Reset(isolate, args[0].As<v8::Function>());
  } else if (args.Length() >= 2 && args[1]->IsFunction()) {
		work->callback.Reset(isolate, args[1].As<v8::Function>());
  }

  uv_queue_work(uv_default_loop(), &work->request, findProc, findOverProc);
}

void EngineWrapper::getLastMove(
    const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();
  EngineWrapper *obj = ObjectWrap::Unwrap<EngineWrapper>(args.Holder());
  int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
  bool result = obj->engine.getLastMove(x1, y1, x2, y2);
  if (result) {
    v8::Local<v8::ArrayBuffer> movement = v8::ArrayBuffer::New(isolate, 4);
    unsigned char *data = (unsigned char *)movement->GetContents().Data();
    data[0] = x1;
    data[1] = y1;
    data[2] = x2;
    data[3] = y2;
    args.GetReturnValue().Set(v8::Uint8Array::New(movement, 0, 4));
  } else {
    args.GetReturnValue().SetNull();
  }
}

// ----------------------------- INITIALIZE -----------------------------------

void EngineWrapper::construct(const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new EngineWrapper(...)`
    EngineWrapper *obj = new EngineWrapper();
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `EngineWrapper(...)`, turn into construct call.
    const int argc = 1;
    v8::Local<v8::Value> argv[argc] = {args[0]};
    v8::Local<v8::Function> cons =
        v8::Local<v8::Function>::New(isolate, constructor);
    args.GetReturnValue().Set(cons->NewInstance(argc, argv));
  }
}

void EngineWrapper::newInstance(
    const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();

  const unsigned argc = 1;
  v8::Local<v8::Value> argv[argc] = {args[0]};
  v8::Local<v8::Function> cons =
      v8::Local<v8::Function>::New(isolate, constructor);
  v8::Local<v8::Object> instance = cons->NewInstance(argc, argv);

  args.GetReturnValue().Set(instance);
}

void EngineWrapper::init(v8::Isolate *isolate) {
  // Prepare constructor template
  v8::Local<v8::FunctionTemplate> tpl =
      v8::FunctionTemplate::New(isolate, construct);
  tpl->SetClassName(v8::String::NewFromUtf8(isolate, "CCEngine"));
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Prototype
  NODE_SET_PROTOTYPE_METHOD(tpl, "startup", startup);
  NODE_SET_PROTOTYPE_METHOD(tpl, "move", move);
  NODE_SET_PROTOTYPE_METHOD(tpl, "undo", undo);
  NODE_SET_PROTOTYPE_METHOD(tpl, "getMoveCount", getMoveCount);
  NODE_SET_PROTOTYPE_METHOD(tpl, "getPlayer", getPlayer);
  NODE_SET_PROTOTYPE_METHOD(tpl, "getState", getState);
  NODE_SET_PROTOTYPE_METHOD(tpl, "findSolutionSync", findSolutionSync);
	NODE_SET_PROTOTYPE_METHOD(tpl, "findSolution", findSolution);
  NODE_SET_PROTOTYPE_METHOD(tpl, "getLastMove", getLastMove);

  EngineWrapper::constructor.Reset(isolate, tpl->GetFunction());
}

void createObject(const v8::FunctionCallbackInfo<v8::Value> &args) {
  EngineWrapper::newInstance(args);
}

void doSleep(const v8::FunctionCallbackInfo<v8::Value> &args) {
  v8::Isolate *isolate = args.GetIsolate();
  if (args.Length() != 1 || !args[0]->IsNumber()) {
    isolate->ThrowException(v8::Exception::TypeError(v8::String::NewFromUtf8(
        isolate, "Invalid call of 'sleep': should provide "
                 "an number as arguments.")));
    return;
  }
#ifdef WIN32
	Sleep(args[0]->Uint32Value());
#else
  sleep(args[0]->Uint32Value());
#endif
}

void init(v8::Local<v8::Object> exports, v8::Local<v8::Object> module) {
  v8::Isolate *isolate = exports->GetIsolate();
  EngineWrapper::init(isolate);

  // exports->Set(v8::String::NewFromUtf8(isolate, "CCEngine"),
  // tpl->GetFunction());

  NODE_SET_METHOD(exports, "create", createObject);
  NODE_SET_METHOD(exports, "sleep", doSleep);
}

NODE_MODULE(ccengine, init)

} // namespace ccengin
